Cel-Shading von Sami "MENTAL" Hamlaoui
Da mich immer noch Leute per E-Mail nach dem Source Code zu dem Artikel fragen, den ich für GameDev.net vor einiger Zeit geschrieben habe und ich absehe, dass eine 2te Version des Artikels (mit Source für jegliches API) noch nicht einmal halbwegs fertig ist, habe ich dieses Tutorial für NeHe zusammengehackt (das war eigentlich die ursprüngliche Absicht des Artikels), so dass allen OpenGL Gurus damit herumspielen können. Entschuldigen Sie die Wahl des Models, aber ich spiele Quake 2 zur Zeit recht ausgiebig... :)
Anmerkung: Der original Artikel zu diesem Code kann unter http://www.gamedev.net/reference/programming/features/celshading gefunden werden.
Dieses Tutorial erklärt eigentlich nicht die Theorie sondern den Code. WARUM es funktioniert, finden Sie im oberen Link. Und nun noch mal laut: HÖRT AUF MIR WEGEN DES SOURCE CODES ZU MAILEN!!!!
Genießt es :).
Als erstes müssen wir ein paar extra Header-Dateien einbinden. Die erste (math.h) ist für die Benutzung der sqrtf (Wurzel) Funktion und die zweite (stdio.h) ist für den Dateizugriff.
#include < math.h> // Header Datei für die Math Library #include < stdio.h> // Header Datei für die Standard I/O Library
typedef struct tagMATRIX // Eine Struktur die eine OpenGL Matrix aufnimmt
{
float Data[16]; // Wir benutzen [16] wegen des OpenGL's Matrix Format
}
MATRIX;
typedef struct tagVECTOR // Eine Struktur die einen einzelnen Vektor enthält
{
float X, Y, Z; // Die Komponenten des Vektors
}
VECTOR;
typedef struct tagVERTEX // Eine Struktur die einen einzelnen Vertex enthält
{
VECTOR Nor; // Vertex Normalenvektor
VECTOR Pos; // Vertex Position
}
VERTEX;
typedef struct tagPOLYGON // Eine Struktur die ein einzelnes Polygon enthält
{
VERTEX Verts[3]; // Array von 3 VERTEX Strukturen
}
POLYGON;
bool outlineDraw = true; // Flag um den Umriß zu zeichnen
bool outlineSmooth = false; // Flag Anti-Alias für die Linien anzuwenden
float outlineColor[3] = { 0.0f, 0.0f, 0.0f }; // Farbe der Linien
float outlineWidth = 3.0f; // Breite der Linien
VECTOR lightAngle; // Die Richtung des Lichts
bool lightRotate = false; // Flag um zu sehen, ob wir das Licht rotieren
float modelAngle = 0.0f; // Y-Achse Winkel des Modells
bool modelRotate = false; // Flag um das Modell zu rotieren
POLYGON *polyData = NULL; // Polygon Daten
int polyNum = 0; // Anzahl der Polygone
GLuint shaderTexture[1]; // Speicherplatz für eine Textur
BOOL ReadMesh () // liest den Inhalt der Datei "model.txt"
{
FILE *In = fopen ("Data\\model.txt", "rb"); // öffne die Datei
if (!In)
return FALSE; // gebe FALSE zurück, wenn Datei nicht geöffnet wurde
fread (&polyNum, sizeof (int), 1, In); // lese den Header (z.B. Anzahl der Polygone)
polyData = new POLYGON [polyNum]; // Alloziiere den Speicher
fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In);// lese alle Polygon-Daten ein
fclose (In); // Schließe die Datei
return TRUE; // hat funktioniert
}
inline float DotProduct (VECTOR &V1, VECTOR &V2) // berechne den Winkel zwischen 2 Vektoren
{
return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z; // gebe den Winkel zurück
}
inline float Magnitude (VECTOR &V) // berechne die Länge des Vektors
{
return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z); // gebe die Länge des Vektors zurück
}
void Normalize (VECTOR &V) // erzeugt einen Vektor mit einer Längeneinheit von 1
{
float M = Magnitude (V); // berechne die Länge des Vektors
if (M != 0.0f) // Stelle sicher, dass wir nicht durch 0 dividieren
{
V.X /= M; // Normalisiere die 3 Komponenten
V.Y /= M;
V.Z /= M;
}
}
void RotateVector (MATRIX &M, VECTOR &V, VECTOR &D) // Rotiere einen Vektor um die angegebene Matrix
{
D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8] * V.Z); // Rotiere um die X Achse
D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9] * V.Z); // Rotiere um die Y Achse
D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z); // Rotiere um die Z Achse
}
// Jeder GL Init Code & Benutzer Initialiasierung kommt hier hin
BOOL Initialize (GL_Window* window, Keys* keys)
{
char Line[255]; // Speicherplatz für 255 Zeichen float shaderData[32][3]; // Speicherplatz für die 96 Shader Werte FILE *In = NULL; // Datei Zeiger
glShadeModel (GL_SMOOTH); // aktiviert weiches Farb-Shading glDisable (GL_LINE_SMOOTH); // deaktiviere anfangs das Weichzeichnen von Linien glEnable (GL_CULL_FACE); // aktiviere OpenGL Face Culling
glDisable (GL_LIGHTING); // deaktiviere OpenGL Beleuchtung
In = fopen ("Data\\shader.txt", "r"); // öffne die Shader-Datei
if (In) // überprüfe, ob die Datei geöffnet wurde
{
for (i = 0; i < 32; i++) // durchlaufe alle 32 grauskalierte Werte
{
if (feof (In)) // Überprüfe auf das Ende der Datei
break;
fgets (Line, 255, In); // hole die aktuelle Zeile
// kopiere über den Wert shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = atof (Line); } fclose (In); // Schließe die Datei } else return FALSE; // Das lief furchtbar schief
glGenTextures (1, &shaderTexture[0]); // hole eine freie Textur ID glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // Binde diese Textur. Von nun an wird sie 1D sein // Um es nochmals zu betonen: Lassen Sie OpenGL keine Bi/Trilineare Filter verwenden! glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Upload glTexImage1D (GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData);
lightAngle.X = 0.0f; // Setze die X Richtung lightAngle.Y = 0.0f; // Setze die Y Richtung lightAngle.Z = 1.0f; // Setze die T Richtung Normalize (lightAngle); // Normalisiere die Licht Richtung
return ReadMesh (); // gebe den Wert von ReadMesh zurück }
void Deinitialize (void) // Jede Benutzer DeInitialisation kommt hier rein
{
glDeleteTextures (1, &shaderTexture[0]); // lösche die Shader-Textur
delete [] polyData; // lösche die Polygon-Daten
}
void Update (DWORD milliseconds) // die Aktualisierung der Bewegung erfolgt hier
{
if (g_keys->keyDown [' '] == TRUE) // Wurde die Leertaste gedrückt?
{
modelRotate = !modelRotate; // Schalte Rotation an/aus
g_keys->keyDown [' '] = FALSE;
}
if (g_keys->keyDown ['1'] == TRUE) // Ist die Taste 1 gedrückt worden?
{
outlineDraw = !outlineDraw; // Schalte das Zeichnen des Umrisses an/aus
g_keys->keyDown ['1'] = FALSE;
}
if (g_keys->keyDown ['2'] == TRUE) // Ist die Taste 2 gedrückt worden?
{
outlineSmooth = !outlineSmooth; // Schalte Anti-Aliasing an/aus
g_keys->keyDown ['2'] = FALSE;
}
if (g_keys->keyDown [VK_UP] == TRUE) // Wurde die Pfeil-nach-oben Taste gedrückt?
{
outlineWidth++; // Erhöhe Linienbreite
g_keys->keyDown [VK_UP] = FALSE;
}
if (g_keys->keyDown [VK_DOWN] == TRUE) // Wurde die Pfeil-nach-unten Taste gedrückt?
{
outlineWidth--; // vermindere die Linienbreite
g_keys->keyDown [VK_DOWN] = FALSE;
}
if (modelRotate) // überprüfe ob Rotation aktiviert ist
modelAngle += (float) (milliseconds) / 10.0f; // aktualisiere Winkel basierend auf der Zeit
}
void Draw (void)
{
float TmpShade; // Temporärer Shader Wert MATRIX TmpMatrix; // Temporäre MATRIX Struktur VECTOR TmpVector, TmpNormal; // Temporäre VECTOR Strukturen
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Lösche die Buffer glLoadIdentity (); // Resette die Matrix
if (outlineSmooth) // überprüfe, ob wir Anti-Aliased Linien haben wollen
{
glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); // benutze die guten Berechnungen
glEnable (GL_LINE_SMOOTH); // aktiviere Anti-Aliasing
}
else // Wir wollen keine weichen Linien
glDisable (GL_LINE_SMOOTH); // deaktiviere Anti-Aliasing
glTranslatef (0.0f, 0.0f, -2.0f); // bewege 2 Einheiten in den Bildschirm hinein glRotatef (modelAngle, 0.0f, 1.0f, 0.0f); // Rotatiere das Modell auf seiner Y-Achse glGetFloatv (GL_MODELVIEW_MATRIX, TmpMatrix.Data); // hole die erzeugte Matrix
// Cel-Shading Code glEnable (GL_TEXTURE_1D); // aktiviere 1D Texturing glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // Binde unsere Textur glColor3f (1.0f, 1.0f, 1.0f); // Setze die Farbe des Modells
glBegin (GL_TRIANGLES); // Teile OpenGL mit, dass wir Dreiecke zeichnen werden
for (i = 0; i < polyNum; i++) // durchlaufe jedes Polygon
{
for (j = 0; j < 3; j++) // durchlaufe jeden Vertex
{
TmpNormal.X = polyData[i].Verts[j].Nor.X; // Fülle die TmpNormal Struktur mit den
TmpNormal.Y = polyData[i].Verts[j].Nor.Y; // Normalenvektorwerten der Vertices
TmpNormal.Z = polyData[i].Verts[j].Nor.Z;
// Rotiere das um die Matrix RotateVector (TmpMatrix, TmpNormal, TmpVector); Normalize (TmpVector); // Normalisiere den neuen Normalenvektor
// berechne den Shade Wert TmpShade = DotProduct (TmpVector, lightAngle); if (TmpShade < 0.0f) TmpShade = 0.0f; // setze den Wert auf 0, wenn negativ
glTexCoord1f (TmpShade); // Setze die Textur Ko-Koordinate wie den Shade Wert // Sende die Vertex Position glVertex3fv (&polyData[i].Verts[j].Pos.X); } } glEnd (); // Telie OpenGL mit, dass wir fertig mit Zeichnen sind glDisable (GL_TEXTURE_1D); // deaktiviere 1D Textur
// Outline Code
if (outlineDraw) // Überprüfe ob wir den Umriß zeichnen wollen
{
glEnable (GL_BLEND); // aktiviere Blending
// Setze den Blend Modus
glBlendFunc (GL_SRC_ALPHA ,GL_ONE_MINUS_SRC_ALPHA);
glPolygonMode (GL_BACK, GL_LINE); // zeichne abgewandte Polygone als Drahtgitter
glLineWidth (outlineWidth); // Setze die Linienbreite
glCullFace (GL_FRONT); // zeichne keine nach vorn gerichtetenPolygone
glDepthFunc (GL_LEQUAL); // ändere den Depth Modus
glColor3fv (&outlineColor[0]); // Setze die Umriß-Farbe
glBegin (GL_TRIANGLES); // Teile OpenGL mit, was wir zeichnen wollen
for (i = 0; i < polyNum; i++) // durchlaufe jedes Polygon
{
for (j = 0; j < 3; j++) // durchlaufe jeden Vertex
{
// Sende die Vertex Position
glVertex3fv (&polyData[i].Verts[j].Pos.X);
}
}
glEnd (); // Teile OpenGL mit, dass wir fertig sind
glDepthFunc (GL_LESS); // Resette den Depth-Test Modus glCullFace (GL_BACK); // Resette die Seite, die ausgewählt werden soll glPolygonMode (GL_BACK, GL_FILL); // Resette den abgewandte Polygone Zeichnen-Modus glDisable (GL_BLEND); // deaktiviere Blending } }